/*
    Dominate 3D - A cube-based game of life

    Copyright (C) 2007, Craig H. Miller (kidmosey@gmail.com)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
/*
TODO LIST:
Version 1.0:
    X BUG: non-reproducable crash at exit of revision 27/28
    X BUG: ingame window does not recieve focus
    X BUG: game runs before everyone has joined.
    X BUG: Server dialog should say "Start", instead of "Save"
    X BUG: Crash with loaded robots running before game has started

    X Refactor and create a game object
    X HUD
    X Main Menu/Menu system
    X In game menu
    X Scoring/Gameplay Options Menu
    X Scoring
    X Font rendering: Score/Timer Display
    X Timer scoring
    X Refactor
    X AI
    X End game
    X Networking
        - Operate through a firewall
    X Multiplayer
        X BUG: Server::EndServerThread is not thread safe
        X BUG: Server thread never exits.

    (Need to redo some of the above for multiplayer mode)
    X HUD
    X Implement round timers
    X Main Menu/Menu system
        - BUG: Tabbing through dialogs is out of order
        X Client connect menu
            X BUG: cancelling client connect crashes game due to blocking in constructor
        X Combine server into options menu
        X loop the game/dialogs
        X In game menu
    X Scoring
        X by cells owned
        X by cells taken
        X by time remaining
    X Enable 2-24 players (1-24 in debug mode)
        X Make scores fit on screen
    X Player join: can't join when game is in progress
    - Add option to allow players to join game in progress
        - Do not fill already filled cells.
    X Player exit: Clear player's current cells.
        X BUG: if player is current player, leaving will end/crash the game.
        X BUG: if player is current player, leaving will keep cells highlighted for next player.
    X Font rendering: Score/Timer Display
    X End game
    X Reset player colors on server restart
    X MP chat
        X make chat box fade with text.
        X include the player's name
        - wrap lines
    X application icon
    - Refactor / comment
        - extract main game loop into object
        X Refactor server code to an object.
            - complete server object
        - ensure all destructors are doing their jobs correctly
    X Add mirror, to see the opposite side of the cube
    X Arbitrary number of cells per face (still cubed, though)
    X Extract thread object
    X Save settings between sessions
    - Extract hard-coded data (width/height/fullscreen) into settings
    - Extract window title to global define

    X Simple AI
        - Personality based
        - Customizable
        - Add option to see thought process
        X Add option in server creation for number of AI players

    X convert threads to objects
    - Redo initial player positions (players to opposite sides of cube)
        - Cube editor
    X Better font
    - Sound FX
    - Better graphics
    - Multiple cubes

    - make code portable
        - threads
        - resources
        - dialogs
*/
#include <algorithm>
#include <functional>
#include <vector>

#include <map>
#include <string>
#include <set>

#include "dominate3d.h"
#include "game.h"
#include "glutil.h"
#include "cell.h"
#include "player.h"
#include "client.h"

#include "resource/resource.h"

float g_FPS; // seconds per frame
bool g_bApplicationFinished = false;
SharedObject<Cube> g_Cube;
SharedObject<Client> g_Client;

// declarations for our dialog procedures
extern INT_PTR CALLBACK JoinDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
extern INT_PTR CALLBACK MainDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
extern INT_PTR CALLBACK OptionDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);

int InitWinsock()
{
    WSADATA wsaData;
    WORD version;

    version = MAKEWORD( 2, 0 );

    /* check for error */
    if ( WSAStartup( version, &wsaData ) != 0 )
    {
        /* error occured */
        return -1;
    }

    /* check for correct version */
    if ( LOBYTE( wsaData.wVersion ) != 2 ||
         HIBYTE( wsaData.wVersion ) != 0 )
    {
        /* incorrect WinSock version */
        WSACleanup();
        return -1;
    }

    /* WinSock has been initialized */
    return 0;
}

void RegisterWndClass()
{
    WNDCLASSEX wc = {
        sizeof(WNDCLASSEX), // UINT cbSize;
        0, // UINT style;
        DefWindowProc, // WNDPROC lpfnWndProc;
        0, // int cbClsExtra;
        0, // int cbWndExtra;
        GetModuleHandle(NULL), // HINSTANCE hInstance;
        (HICON )LoadIcon(NULL, MAKEINTRESOURCE(IDI_APP)), // HICON hIcon;
        0, // HCURSOR hCursor;
        GetSysColorBrush(COLOR_3DFACE), // HBRUSH hbrBackground;
        0, // LPCTSTR lpszMenuName;
        "Dominate3D", // LPCTSTR lpszClassName;
        0 // HICON hIconSm;
    };

    RegisterClassEx(&wc);
}

void UnregisterWndClass()
{
    UnregisterClass("Dominate3D", GetModuleHandle(NULL));
}

namespace ClientFunctions
{
    extern std::map<TMessage, Client::MessageFunc> MESSAGEFUNC;
};

class SocketListener: public Thread::ThreadObject
{
public:
    SocketListener()
    {
        printf("CLIENT: Starting listen thread\n");
    }

    ~SocketListener()
    {
        printf("CLIENT: Exiting listen thread\n");
    }

    void Run()
    {
        while (!m_finished)
        {
            Client &cl = g_Client.Lock();
            int status = CheckReadStatus(cl.GetSocket());

            if (status == 1)
                cl.ReadMessage(ClientFunctions::MESSAGEFUNC);
            else if (status == SOCKET_ERROR)
                m_finished = true;

            g_Client.Unlock();

            Sleep(1);
        }
    }
};

namespace Server
{
    extern SharedObject<Cube> g_Cube;
    extern void IncrementPlayer();
    extern bool s_bGameStarted;
};

typedef struct _AIPersonality
{
    // offensive or defensive?
    int aggression;

    // whether we want to go after winners or losers
    int score_balance;

    // do we attack the people who attack us the most?
    int vengeance;
} AIPersonality;

#define MAX_PERSONALITY_VALUE 100
#define NEUTRAL_PERSONALITY_VALUE (MAX_PERSONALITY_VALUE/2)

bool CmpMoveType(const CellStats &a, const CellStats &b)
{
    return (a.moveType < b.moveType);
}

bool CmpTotalCells(const CellStats &a, const CellStats &b)
{
    return a.total_cells_taken < b.total_cells_taken;
}

bool CmpVulnMove(const CellStats &a, const CellStats &b)
{
    return a.cells_vulnerable_move < b.cells_vulnerable_move;
}

bool CmpVulnNoMove(const CellStats &a, const CellStats &b)
{
    return a.cells_vulnerable_nomove < b.cells_vulnerable_nomove;
}

// SORT ORDER:
// loser_cells_taken => total_cells_taken => cells_vulnerable_move => moveType => cells_vulnerable_nomove
bool CmpAggression(const CellStats &a, const CellStats &b)
{
    bool cmp[] = {
        CmpTotalCells(a,b),
        CmpVulnMove(a,b),
        CmpMoveType(a,b)
    };

    if (!cmp[0])
    {
        if (!cmp[1])
            return cmp[2];

        return cmp[1];
    }

    return cmp[0];
}

// Sort ascending by ammount of loser cell's taken.
Player *g_SortPlayer;
bool CmpPlayerCellsTaken(const CellStats &a, const CellStats &b)
{
    assert(g_SortPlayer);

    std::map<Player *, int>::const_iterator
        iterA = a.cells_taken.find(g_SortPlayer),
        iterB = b.cells_taken.find(g_SortPlayer);

    if (iterB != a.cells_taken.end())
    {
        if (iterA != b.cells_taken.end())
            // a and b exist
            return (*iterA).second < (*iterB).second;

        // only b exists
        return true;
    }

    // b doesn't exist, automatically loses.
    return false;
}

std::vector<CellStats> FilterStats(const std::vector<CellStats> &stats, const int measure, bool     (*cmpFunc)(const CellStats &, const CellStats &))
{
    assert(cmpFunc);

    std::vector<CellStats> result;

    std::vector<CellStats> tmp = stats;
    std::sort(tmp.begin(), tmp.end(), cmpFunc);

    if (measure < NEUTRAL_PERSONALITY_VALUE) // defensive
    {
        float keep = ((float )measure / (float )NEUTRAL_PERSONALITY_VALUE) * (float )(tmp.size()/2);
        int idx = (keep > 1.0f) ? (int )keep : 1;

        //result.erase(result.begin(), result.end() - idx);
        std::copy(tmp.begin(), tmp.begin() + idx, std::back_inserter(result));
    }
    else if (measure > NEUTRAL_PERSONALITY_VALUE) // offensive
    {
        float keep = ((float )(MAX_PERSONALITY_VALUE-measure) / (float )NEUTRAL_PERSONALITY_VALUE) * (float )(tmp.size()/2);
        int idx = (keep > 1.0f) ? (int )keep : 1;

        //result.erase(result.begin() + idx, result.end());
        std::copy(tmp.rbegin(), tmp.rbegin() + idx, std::back_inserter(result));
    }
    else
    {
        result = stats;
        // at least give them something that isn't completely random.
        std::sort(result.begin(), result.end(), CmpMoveType);
        if (result.size() > 2)
            result.erase(result.begin(), result.begin() + (result.size()/2));
    }

    return result;
}

std::vector<CellStats> FilterStatsByPersonality(const std::vector<CellStats> &stats, const AIPersonality &ai)
{
    std::vector<CellStats> result = stats;

    std::sort(result.begin(), result.end(), CmpTotalCells);

    if (result.size() > 3)
        result.erase(result.begin(), result.end() - 3);

    return result;

    // Grab the aggression list.
    std::vector<CellStats> ag_list;
    if (ai.aggression != NEUTRAL_PERSONALITY_VALUE)
        ag_list = FilterStats(stats, ai.aggression, CmpAggression);
/*
   // append the bully/balance list
    std::vector<CellStats> sb_list;
    if (ai.score_balance < NEUTRAL_PERSONALITY_VALUE)
    {
        g_SortPlayer = g_currentGame->GetLoser(g_currentGame->GetCurrentPlayer());
        sb_list = FilterStats(stats, MAX_PERSONALITY_VALUE - ai.score_balance, CmpPlayerCellsTaken);
    }
    else if (ai.score_balance > NEUTRAL_PERSONALITY_VALUE)
    {
        g_SortPlayer = g_currentGame->GetWinner(g_currentGame->GetCurrentPlayer());
        sb_list = FilterStats(stats, ai.score_balance, CmpPlayerCellsTaken);
    }
*/
//  std::copy(ag_list.begin(), ag_list.end(), std::back_inserter(result));
//  std::copy(sb_list.begin(), sb_list.end(), std::back_inserter(result));

    //if (result.size() == 0)
    //    result = FilterStats(stats, NEUTRAL_PERSONALITY_VALUE, CmpMoveType);

    return result;
}
/*
void SolveWithAI()
{
    assert(g_currentGame);

    // Game Over
    if (g_currentGame->GameOver() && g_TimerAI)
    {
        KillTimer(0, g_TimerAI);
        g_TimerAI = 0;
        return;
    }

    Player *player = g_currentGame->GetCurrentPlayer();
    assert(player);

    // Get a list of copy_to cells.
    std::vector<CellStats> c2cells = g_currentGame->m_cube.GetCellStats(player);

    // Filter the list for player 1
    if (g_currentGame->GetCurrentPlayer() == g_currentGame->GetPlayer(0) ||
        g_currentGame->GetCurrentPlayer() == g_currentGame->GetPlayer(1))
    {
        c2cells = FilterStatsByPersonality(c2cells, AI[0]);
    }

    if (c2cells.size() > 0)
    {
        CellStats *c = &c2cells[rand() % c2cells.size()];
        assert(c);
        assert(c->moveFrom);
        assert(c->moveTo  );

        c->moveFrom->Select();

        if (c->moveType == Cell::JUMP_TO)
            c->moveFrom->SetPlayer(0);

        c->moveTo->TakeOver(player);

        c->moveFrom->Unselect();
    }

    g_currentGame->IncrementPlayer();
}
*/

class AIObject
{
public:
    AIObject()
    {
        char *serverAddress = "127.0.0.1";

        int connected = m_client.Connect(
            serverAddress,
            atoi(GetOption("ServerPort").c_str()));

        if (connected == SOCKET_ERROR)
        {
            printf("AIObject::Connect() ERROR: %s", GetLastErrorStr(""));
            return;
        }

        m_client.RegisterBot("Robot " + itoa(s_BotNum++));
    }

    ~AIObject()
    {
        closesocket(m_client.GetSocket());
    }

    class NotMine
    {
    public:
        NotMine(Player *player): m_player(player)
        {
        }

        bool operator() (Cell *cell)
        {
            return (cell->GetPlayer() != m_player);
        }

    private:
        Player *m_player;
    };

    std::vector<Cell *> GetMyCells() const
    {
        std::vector<Cell *> result;

        Player *me = GetPlayer();
        if (me)
        {
            Cube &cube = Server::g_Cube.Lock();

            std::remove_copy_if(
                cube.m_listCells.begin(),
                cube.m_listCells.end(),
                std::inserter(result, result.end()),
                NotMine(me));

            Server::g_Cube.Unlock();
        }

        return result;
    }

    std::pair<Cell*, Cell*> m_bestMove;
    void CalculateMove()
    {
        AIPersonality AI[] = {
            { 100, 100, 100 }
        };

        printf("BOT[%d]: Calculating my move...\n", m_client.GetID());

        Player *me = GetPlayer();
        if (me)
        {
            std::vector<CellStats> c2cells = Server::g_Cube.Lock().GetCellStats(me);
            Server::g_Cube.Unlock();

            c2cells = FilterStatsByPersonality(c2cells, AI[0]);

            if (c2cells.size() == 0)
            {
                c2cells = Server::g_Cube.Lock().GetCellStats(me);
                Server::g_Cube.Unlock();
            }

            if (c2cells.size() > 0)
            {
                const CellStats &c = c2cells[rand() % c2cells.size()];
                assert(c.moveFrom);
                assert(c.moveTo  );

                m_bestMove = std::pair<Cell*,Cell*>(c.moveFrom, c.moveTo);
            }
            else
            {
                m_bestMove = std::pair<Cell*,Cell*>(0,0);
            }
        }
    }

    void PerformMove()
    {
        if (m_bestMove.first && m_bestMove.second)
        {
            printf("BOT[%d]: Making my move...\n", m_client.GetID());

            SocketParam p1[] = { itoa(m_bestMove.first->GetID()) };
            m_client.SendMessage(SV_PICKCELL, std::vector<SocketParam>(p1, p1+COUNT(p1)));
#ifdef NDEBUG
            // TODO: Add option to view PC thinking
            Sleep(1000);
#endif
            SocketParam p2[] = { itoa(m_bestMove.second->GetID()) };
            m_client.SendMessage(SV_PICKCELL, std::vector<SocketParam>(p2, p2+COUNT(p2)));
        }
        else
        {
            printf("BOT[%d]: Too confused to move...\n", m_client.GetID());
            Server::IncrementPlayer();
        }
    }

    Player *GetPlayer() const
    {
        Player *p = Server::g_Cube.Lock().GetPlayer(m_client.GetID());
        Server::g_Cube.Unlock();

        return p;
    }

    const Client &GetClient() const { return m_client; }

private:
    static int s_BotNum;
    Client m_client;
};
int AIObject::s_BotNum = 0;

void SocketFlush(SOCKET sockfd)
{
    while (CheckReadStatus(sockfd) == 1)
    {
        char buffer[256];
        recv(sockfd, buffer, sizeof(buffer), 0);
    }
}

void CheckAI(AIObject *ai)
{
    if (ai)
    {
        Player *p = Server::g_Cube.Lock().GetCurrentPlayer();
        Server::g_Cube.Unlock();

        if (p == ai->GetPlayer())
        {
            ai->CalculateMove();
            ai->PerformMove();
        }

        SocketFlush(ai->GetClient().GetSocket());
    }
}

class AIManager: public Thread::ThreadObject
{
public:
    AIManager(int numBots)
    {
        while (numBots--)
            m_listObjects.push_back(new AIObject());
    }

    ~AIManager()
    {
    }

    void Run()
    {
        while (!m_finished)
        {
            if (Server::s_bGameStarted)
                std::for_each(m_listObjects.begin(), m_listObjects.end(), CheckAI);
            Sleep(100);
        }
    }

private:
    std::vector<AIObject *> m_listObjects;
};

int StartGame()
{
    // create the status window/thread
    Thread statusWindowThread(new StatusWindow());
#define STATUS_WINDOW GetThreadObject<StatusWindow>(statusWindowThread)

    // load the client
    STATUS_WINDOW->Update("Creating a client...");
    g_Client.Reset(new Client());

    // Connect to the server.
    const char *serverAddress = GetOption("ServerAddress").c_str();
    if (OptionIsEnabled("IsServer"))
        serverAddress = "127.0.0.1";

    STATUS_WINDOW->Update("Connecting to the server...");
    int connected = g_Client.Lock().Connect(
            serverAddress,
            atoi(GetOption("ServerPort").c_str()));
    g_Client.Unlock();

    if (connected == SOCKET_ERROR)
    {
        printf("CLIENT::Connect() ERROR: %s", GetLastErrorStr(""));

        while (connected == SOCKET_ERROR && IsWindow(STATUS_WINDOW->GetWindow()))
        {
            connected = g_Client.Lock().Connect(
                serverAddress,
                atoi(GetOption("ServerPort").c_str()));
            g_Client.Unlock();
        }
    }
    if (connected == SOCKET_ERROR) return IDD_MAIN;

    STATUS_WINDOW->Update("Registering the player...");
    g_Client.Lock().RegisterPlayer();
    g_Client.Unlock();

    STATUS_WINDOW->Update("Initializing the cube...");
    g_Cube.Reset(new Cube(atoi(GetOption("CubeDimension").c_str())));

    // begin our client listen thread
    STATUS_WINDOW->Update("Creating listening thread...");
    Thread clientThread(new SocketListener());

    // adding bots
    Thread *aiThread = 0;
    if (OptionIsEnabled("IsServer"))
    {
        STATUS_WINDOW->Update("Adding the bots...");
        int maxBots = atoi(GetOption("NumPlayers").c_str()) - 1;
        int numBots = atoi(GetOption("NumBots"   ).c_str());

        aiThread = new Thread(new AIManager(std::min(numBots,maxBots)));
    }

    // wait for the game to start.
    int numPlayers = -1;
    while (IsWindow(STATUS_WINDOW->GetWindow()))
    {
        int count = g_Cube.Lock().GetNumPlayers();
        g_Cube.Unlock();

        if (count != numPlayers)
        {
            numPlayers = count;

            char msg[64];
            sprintf(msg,
                    "Waiting for players (%d remaining).",
                    atoi(GetOption("NumPlayers").c_str()) - numPlayers);
            STATUS_WINDOW->Update(msg);
        }

        if (CheckReadStatus(g_Client.Lock().GetSocket()) == SOCKET_ERROR)
        {
            g_Client.Unlock();
            STATUS_WINDOW->Update("Unable to join game.");
            break;
        }
        g_Client.Unlock();

        if (atoi(GetOption("NumPlayers").c_str()) == numPlayers)
        {
            STATUS_WINDOW->m_finished = true;
            break;
        }

        Sleep(100);
    }
    statusWindowThread.WaitForExit();

    // start the game
    int result = IDD_MAIN;
    if (CheckReadStatus(g_Client.Lock().GetSocket()) != SOCKET_ERROR)
    {
        g_Client.Unlock();
        result = Game::DoLoop();
    }
    else
        g_Client.Unlock();

    closesocket(g_Client.Lock().GetSocket());
    g_Client.Unlock();

    // Kill all of our bots
    delete aiThread;

    return result;
#undef STATUS_WINDOW
}

void DoTestCube()
{
    g_Cube.Reset(new Cube(atoi(GetOption("CubeDimension").c_str())));

    Cube &cube = g_Cube.Lock();

    int numPlayers = 24;
    for (int n = 0; n < numPlayers; n++)
    {
        char tmp[256];
        sprintf(tmp, "Player %d", n + 1);
        Player *player = new Player(tmp);
        cube.AddPlayer(player);

        std::vector<Cell *> startCells = cube.GetStartCells(player);
        std::vector<Cell *>::iterator i = startCells.begin();
        while (i != startCells.end())
        {
            (*i)->SetPlayer(player);
            *i++;
        }
    }
    g_Cube.Unlock();
    Game::DoLoop();
}

int main()
{
    InitWinsock();
    InitCommonControls();
    RegisterWndClass();
    ReadOptionsFromFile("settings.txt");

    std::map<int, DLGPROC> listDialogs;

    listDialogs[IDD_MAIN   ] = MainDlgProc;
    listDialogs[IDD_JOIN   ] = JoinDlgProc;
    listDialogs[IDD_OPTIONS] = OptionDlgProc;

    int currentDlg = IDD_MAIN;
    while (currentDlg)
    {
        assert(listDialogs.find(currentDlg) != listDialogs.end());

        currentDlg = DialogBox(
            GetModuleHandle(NULL),
            MAKEINTRESOURCE(currentDlg),
            GetActiveWindow(),
            listDialogs[currentDlg]);

        if (currentDlg == IDD_JOIN)
            SetOption("IsServer", "false");
        else if (currentDlg == IDD_OPTIONS)
            SetOption("IsServer", "true");

        if (currentDlg == 1)
        {
            if (OptionIsEnabled("IsServer"))
                Server::StartServerThread(atoi(GetOption("ServerPort").c_str()));

            currentDlg = StartGame();

            if (OptionIsEnabled("IsServer"))
                Server::EndServerThread();
        }
    }

    return 0;
}
